https://www.tidytextmining.com/

load libraries

library(rtweet)
library(tidyverse)
library(tidytext)
library(wordcloud2)

Get Tweets

search_tweets

tweet_collection <- search_tweets("lego star wars", n=10000, lang = "en")

Downloading [>----------------------------------------]   2%
Downloading [>----------------------------------------]   3%
Downloading [=>---------------------------------------]   4%
Downloading [=>---------------------------------------]   5%
Downloading [=>---------------------------------------]   6%
Downloading [==>--------------------------------------]   7%
Downloading [==>--------------------------------------]   8%
Downloading [===>-------------------------------------]   9%
Downloading [===>-------------------------------------]  10%
Downloading [====>------------------------------------]  11%
Downloading [====>------------------------------------]  12%
Downloading [====>------------------------------------]  13%
Downloading [=====>-----------------------------------]  14%
Downloading [=====>-----------------------------------]  15%
Downloading [======>----------------------------------]  16%
Downloading [======>----------------------------------]  17%
Downloading [======>----------------------------------]  18%
Downloading [=======>---------------------------------]  19%
Downloading [=======>---------------------------------]  20%
Downloading [========>--------------------------------]  21%
Downloading [========>--------------------------------]  22%
Downloading [========>--------------------------------]  23%
Downloading [=========>-------------------------------]  24%
Downloading [=========>-------------------------------]  25%
Downloading [==========>------------------------------]  26%
Downloading [==========>------------------------------]  27%
Downloading [==========>------------------------------]  28%
Downloading [===========>-----------------------------]  29%
Downloading [===========>-----------------------------]  30%
Downloading [============>----------------------------]  31%
Downloading [============>----------------------------]  32%
Downloading [=============>---------------------------]  33%
Downloading [=============>---------------------------]  34%
Downloading [=============>---------------------------]  35%
Downloading [==============>--------------------------]  36%
Downloading [==============>--------------------------]  37%
Downloading [===============>-------------------------]  38%
Downloading [===============>-------------------------]  39%
Downloading [===============>-------------------------]  40%
Downloading [================>------------------------]  41%
Downloading [================>------------------------]  42%
Downloading [=================>-----------------------]  43%
Downloading [=================>-----------------------]  44%
Downloading [=================>-----------------------]  45%
Downloading [==================>----------------------]  46%
Downloading [==================>----------------------]  47%
Downloading [===================>---------------------]  48%
Downloading [===================>---------------------]  49%
Downloading [===================>---------------------]  50%
Downloading [====================>--------------------]  51%
Downloading [====================>--------------------]  52%
Downloading [=====================>-------------------]  53%
Downloading [=====================>-------------------]  54%
Downloading [======================>------------------]  55%
Downloading [======================>------------------]  56%
Downloading [======================>------------------]  57%
Downloading [=======================>-----------------]  58%
Downloading [=======================>-----------------]  59%
Downloading [========================>----------------]  60%
Downloading [========================>----------------]  61%
Downloading [========================>----------------]  62%
Downloading [=========================>---------------]  63%
Downloading [=========================>---------------]  64%
Downloading [==========================>--------------]  65%
Downloading [==========================>--------------]  66%
Downloading [==========================>--------------]  67%
Downloading [===========================>-------------]  68%
Downloading [===========================>-------------]  69%
Downloading [============================>------------]  70%
Downloading [============================>------------]  71%
Downloading [=============================>-----------]  72%
Downloading [=============================>-----------]  73%
Downloading [=============================>-----------]  74%
Downloading [==============================>----------]  75%
Downloading [==============================>----------]  76%
Downloading [===============================>---------]  77%
Downloading [===============================>---------]  78%
Downloading [===============================>---------]  79%
Downloading [================================>--------]  80%
Downloading [================================>--------]  81%
Downloading [=================================>-------]  82%
Downloading [=================================>-------]  83%
Downloading [=================================>-------]  84%
Downloading [==================================>------]  85%
Downloading [==================================>------]  86%
Downloading [===================================>-----]  87%
Downloading [===================================>-----]  88%
Downloading [===================================>-----]  89%
Downloading [====================================>----]  90%
Downloading [====================================>----]  91%
Downloading [=====================================>---]  92%
Downloading [=====================================>---]  93%
tweet_collection.orig <- tweet_collection
tweet_collection <- tweet_collection %>% 
  filter(is_retweet == "FALSE") 

tweet_collection

Tokenize tweets

tweets_by_tweeter <- tweet_collection %>% 
  group_by(screen_name) %>% 
  mutate(line = row_number()) %>% 
  ungroup()

tweets_by_tweeter %>% 
  count(screen_name, sort = TRUE)

# glimpse(tweets_by_tweeter)
{r}
bad_hashtags <- tweets_by_tweeter %>% 
  select(status_id, hashtags) %>% 
  unnest() %>% 
  # filter(str_detect(hashtags, regex("electionnight", ignore_case = TRUE)) |
  #          # str_detect(hashtags, regex("election20", ignore_case = TRUE)) |
  #          str_detect(hashtags, "2020")) %>% 
  distinct(status_id)

bad_hashtags
  # group_by(hashtags) %>% 
  # summarise(tot_status_id = n()) %>% 
  # arrange(-tot_status_id)          # ElectionNight  / Election2020
  # count(hashtags, sort = TRUE)

tweets_by_tweeter <- tweets_by_tweeter %>% 
  anti_join(bad_hashtags)

tweets_by_tweeter

"Because we have kept text such as hashtags and usernames in the dataset, we can’t use a simple anti_join() to remove stop words. Instead, we can take the approach shown in the filter() line that uses str_detect() from the stringr package. – https://www.tidytextmining.com/twitter.html

tweets_tokenized <- tweets_by_tweeter %>% 
  select(text, screen_name, line) %>% 
  unnest_tokens(word, text, token = "tweets") %>%
  filter(!word %in% stop_words$word,
         !word %in% str_remove_all(stop_words$word, "'"),
         str_detect(word, "[a-z]")) 
Using `to_lower = TRUE` with `token = 'tweets'` may not preserve URLs.
tweets_tokenized

stop words

head(stopwordslangs)
tweets_tokenized %>% 
  count(word, sort = TRUE, name = "freq") %>% 
  filter(!str_detect(word, "^\\@")) %>% 
  anti_join(stopwordslangs)           # anti_join(tidytext::get_stopwords())
Joining, by = "word"

Word frequencies

Calculate word frequency

frequency <- tweets_tokenized %>% 
  group_by(screen_name) %>% 
  count(word, sort = TRUE) %>% 
  left_join(tweets_tokenized %>% 
              group_by(screen_name) %>% 
              summarise(total = n())) %>%
  mutate(freq = n/total)
`summarise()` ungrouping output (override with `.groups` argument)
Joining, by = "screen_name"
frequency

"This is a nice and tidy data frame but we would actually like to plot those frequencies on the x- and y-axes of a plot, so we will need to use spread() from tidyr make a differently shaped data frame. – https://www.tidytextmining.com/twitter.html

pivot_wider

frequency <- frequency %>% 
  select(screen_name, word, freq) %>% 
  pivot_wider(names_from = screen_name, values_from = freq) #, values_fill = 0)

frequency 

viz it

tweets_tokenized %>% 
  # group_by(screen_name) %>% 
  count(word, sort = TRUE, name = "freq") %>% 
  head(500) %>%
  filter(!str_detect(word, "^\\@")) %>% 
  anti_join(stopwordslangs)  %>% 
  wordcloud2(size = 2)
Joining, by = "word"
tweets_tokenized %>% 
  count(word, sort = TRUE, name = "freq") %>% 
  filter(!str_detect(word, "^\\@")) %>% 
  slice_head(n = 30) %>% 
  ggplot(aes(freq, fct_reorder(word, freq))) +
  geom_col()


tweets_tokenized %>% 
  count(word, sort = TRUE, name = "freq") %>% 
  anti_join(stopwordslangs) %>%                    # twitter stop words.
  filter(!str_detect(word, "^\\@")) %>% 
  slice_head(n = 30) %>% 
  ggplot(aes(freq, fct_reorder(word, freq))) +
  geom_col()
Joining, by = "word"

ggplot(frequency, aes(RGONZALEZ1978, BrickFanatics)) +
  geom_jitter(alpha = 0.1, size = 2.5, width = 0.25, height = 0.25) +
  geom_text(aes(label = word), check_overlap = TRUE, vjust = 1.5) +
  scale_x_log10(labels = scales::percent_format()) +
  scale_y_log10(labels = scales::percent_format()) +
  geom_abline(color = "firebrick")


# fs::dir_create("images")
# ggsave("images/dukeball.png")

# "CBBCent1" | screen_name == "Adam_Bradford1
# marchmadness  TheAndyKatz

# RGONZALEZ1978
# <dbl>
# BrickFanatics
# <dbl>


ggplot(frequency, aes(BrickFanatics, fantasysite)) +
  geom_jitter(alpha = 0.1, size = 2.5, width = 0.25, height = 0.25) +
  geom_text(aes(label = word), check_overlap = TRUE, vjust = 1.5) +
  scale_x_log10(labels = scales::percent_format()) +
  scale_y_log10(labels = scales::percent_format()) +
  geom_abline(color = "firebrick")

Word Usage

tweets_by_tweeter %>% 
  summarise(min_date = min(created_at), max_date = max(created_at))
word_ratios <- tweets_tokenized %>%
  # filter(screen_name == "CBBCent1" | screen_name == "Adam_Bradford14") %>%
  filter(screen_name == "BrickFanatics" | screen_name == "fantasysite") %>% 
  filter(!str_detect(word, "^@")) %>%
  count(word, screen_name) %>%
  group_by(word) %>%
  filter(sum(n) >= 2) %>%
  ungroup() %>%
  pivot_wider(names_from = screen_name, values_from = n, values_fill = 0) %>%
  mutate_if(is.numeric, list(~(. + 1) / (sum(.) + 1))) %>%
  mutate(logratio = log(BrickFanatics / fantasysite)) %>%
  arrange(desc(logratio))

word_ratios

equal usage

word_ratios %>% 
  arrange(abs(logratio))
word_ratios %>%
  group_by(logratio < 0) %>%  # < 0) %>%
  top_n(15, abs(logratio)) %>%
  ungroup() %>%
  mutate(word = reorder(word, logratio)) %>%
  ggplot(aes(word, logratio, fill = logratio < 0)) +
  geom_col() + #show.legend = FALSE) +
  coord_flip() +
  ylab("log odds ratio (BrickFanatics/fantasysite)") +
  # scale_fill_discrete(name = "", labels = c("BrickFanatics", "fantasysite")) +
  scale_fill_brewer(palette = "Dark2", name = "", labels = c("BrickFanatics", "fantasysite"))

Favorites and retweets

https://www.tidytextmining.com/twitter.html#favorites-and-retweets

Changes in word use

https://www.tidytextmining.com/twitter.html#changes-in-word-use

Term Document Matrix

{r}
# dtm <- DocumentTermMatrix(docs) 

dtm2 <- TermDocumentMatrix(corpus)
m <- as.matrix(dtm2)
v <- sort(rowSums(m),decreasing=TRUE)
d <- data.frame(word = names(v),freq=v)

d <- d %>% 
  slice(2:200)

https://www.tidytextmining.com/tfidf.html#the-bind_tf_idf-function

tweet_words <- tweets_by_tweeter %>% 
  select(screen_name, text, status_id, user_id) %>%  
  unnest_tokens(word, text, token = "tweets") %>% 
  filter(!str_detect(word, "^\\@")) %>%
  filter(!str_detect(word, "^http")) %>%
  anti_join(stopwordslangs)  %>% 
  count(word, tweeter = screen_name, sort = TRUE)
Using `to_lower = TRUE` with `token = 'tweets'` may not preserve URLs.
Joining, by = "word"
tweet_words

total_words <- tweet_words %>%
  group_by(tweeter) %>%
  summarize(total = sum(n)) %>% 
  arrange(-total)
`summarise()` ungrouping output (override with `.groups` argument)
total_words

tweet_words <- left_join(tweet_words, total_words)
Joining, by = "tweeter"
tweet_words
tweet_words %>% 
  bind_tf_idf(word, tweeter, n)
tweet_words %>% 
  bind_tf_idf(word, tweeter, n) %>% 
  arrange(desc(tf_idf)) %>% 
  mutate(word = factor(word, levels = rev(unique(word)))) %>% 
  filter(n > 10) %>% 
  # group_by(tweeter) %>% 
  # top_n(2) %>% 
  # ungroup() %>% 
  ggplot(aes(word, tf_idf)) +
  geom_col() +
  facet_wrap(~ tweeter) +
  coord_flip()

Resource list

LS0tDQp0aXRsZTogIlJ0d2VldCINCnN1YnRpdGxlOiAiYW4gUmZ1biBkZW1vbnN0cmF0aW9uIg0KYXV0aG9yOiAiSm9obiBMaXR0bGUiDQpkYXRlOiAnYHIgU3lzLkRhdGUoKWAnDQpvdXRwdXQ6IGh0bWxfbm90ZWJvb2sNCi0tLQ0KDQpodHRwczovL3d3dy50aWR5dGV4dG1pbmluZy5jb20vDQoNCiMjIGxvYWQgbGlicmFyaWVzDQoNCmBgYHtyIGxpYnJhcmllcywgbWVzc2FnZT1GQUxTRSwgd2FybmluZz1GQUxTRX0NCmxpYnJhcnkocnR3ZWV0KQ0KbGlicmFyeSh0aWR5dmVyc2UpDQpsaWJyYXJ5KHRpZHl0ZXh0KQ0KbGlicmFyeSh3b3JkY2xvdWQyKQ0KYGBgDQoNCg0KDQoNCiMjIEdldCBUd2VldHMgDQoNCnNlYXJjaF90d2VldHMNCmBgYHtyIGdldFR3ZWV0c30NCnR3ZWV0X2NvbGxlY3Rpb24gPC0gc2VhcmNoX3R3ZWV0cygibGVnbyBzdGFyIHdhcnMiLCBuPTEwMDAwLCBsYW5nID0gImVuIikNCg0KdHdlZXRfY29sbGVjdGlvbi5vcmlnIDwtIHR3ZWV0X2NvbGxlY3Rpb24NCmBgYA0KDQoNCmBgYHtyIHByb2Nlc3MgVHdlZXRzfQ0KdHdlZXRfY29sbGVjdGlvbiA8LSB0d2VldF9jb2xsZWN0aW9uICU+JSANCiAgZmlsdGVyKGlzX3JldHdlZXQgPT0gIkZBTFNFIikgDQoNCnR3ZWV0X2NvbGxlY3Rpb24NCmBgYA0KDQoNCiMjIFRva2VuaXplIHR3ZWV0cw0KDQpgYGB7ciBjb3JwdXMydmVjdG9yfQ0KdHdlZXRzX2J5X3R3ZWV0ZXIgPC0gdHdlZXRfY29sbGVjdGlvbiAlPiUgDQogIGdyb3VwX2J5KHNjcmVlbl9uYW1lKSAlPiUgDQogIG11dGF0ZShsaW5lID0gcm93X251bWJlcigpKSAlPiUgDQogIHVuZ3JvdXAoKQ0KDQp0d2VldHNfYnlfdHdlZXRlciAlPiUgDQogIGNvdW50KHNjcmVlbl9uYW1lLCBzb3J0ID0gVFJVRSkNCg0KIyBnbGltcHNlKHR3ZWV0c19ieV90d2VldGVyKQ0KYGBgDQoNCmBgYA0Ke3J9DQpiYWRfaGFzaHRhZ3MgPC0gdHdlZXRzX2J5X3R3ZWV0ZXIgJT4lIA0KICBzZWxlY3Qoc3RhdHVzX2lkLCBoYXNodGFncykgJT4lIA0KICB1bm5lc3QoKSAlPiUgDQogICMgZmlsdGVyKHN0cl9kZXRlY3QoaGFzaHRhZ3MsIHJlZ2V4KCJlbGVjdGlvbm5pZ2h0IiwgaWdub3JlX2Nhc2UgPSBUUlVFKSkgfA0KICAjICAgICAgICAgICMgc3RyX2RldGVjdChoYXNodGFncywgcmVnZXgoImVsZWN0aW9uMjAiLCBpZ25vcmVfY2FzZSA9IFRSVUUpKSB8DQogICMgICAgICAgICAgc3RyX2RldGVjdChoYXNodGFncywgIjIwMjAiKSkgJT4lIA0KICBkaXN0aW5jdChzdGF0dXNfaWQpDQoNCmJhZF9oYXNodGFncw0KICAjIGdyb3VwX2J5KGhhc2h0YWdzKSAlPiUgDQogICMgc3VtbWFyaXNlKHRvdF9zdGF0dXNfaWQgPSBuKCkpICU+JSANCiAgIyBhcnJhbmdlKC10b3Rfc3RhdHVzX2lkKSAgICAgICAgICAjIEVsZWN0aW9uTmlnaHQgIC8gRWxlY3Rpb24yMDIwDQogICMgY291bnQoaGFzaHRhZ3MsIHNvcnQgPSBUUlVFKQ0KDQp0d2VldHNfYnlfdHdlZXRlciA8LSB0d2VldHNfYnlfdHdlZXRlciAlPiUgDQogIGFudGlfam9pbihiYWRfaGFzaHRhZ3MpDQoNCnR3ZWV0c19ieV90d2VldGVyDQpgYGANCg0KDQoNCj4gIkJlY2F1c2Ugd2UgaGF2ZSBrZXB0IHRleHQgc3VjaCBhcyBoYXNodGFncyBhbmQgdXNlcm5hbWVzIGluIHRoZSBkYXRhc2V0LCB3ZSBjYW7igJl0IHVzZSBhIHNpbXBsZSBhbnRpX2pvaW4oKSB0byByZW1vdmUgc3RvcCB3b3Jkcy4gSW5zdGVhZCwgd2UgY2FuIHRha2UgdGhlIGFwcHJvYWNoIHNob3duIGluIHRoZSBmaWx0ZXIoKSBsaW5lIHRoYXQgdXNlcyBzdHJfZGV0ZWN0KCkgZnJvbSB0aGUgc3RyaW5nciBwYWNrYWdlLiAtLSBodHRwczovL3d3dy50aWR5dGV4dG1pbmluZy5jb20vdHdpdHRlci5odG1sDQoNCg0KYGBge3IgdG9rZW5pemVkIHR3ZWV0c30NCnR3ZWV0c190b2tlbml6ZWQgPC0gdHdlZXRzX2J5X3R3ZWV0ZXIgJT4lIA0KICBzZWxlY3QodGV4dCwgc2NyZWVuX25hbWUsIGxpbmUpICU+JSANCiAgdW5uZXN0X3Rva2Vucyh3b3JkLCB0ZXh0LCB0b2tlbiA9ICJ0d2VldHMiKSAlPiUNCiAgZmlsdGVyKCF3b3JkICVpbiUgc3RvcF93b3JkcyR3b3JkLA0KICAgICAgICAgIXdvcmQgJWluJSBzdHJfcmVtb3ZlX2FsbChzdG9wX3dvcmRzJHdvcmQsICInIiksDQogICAgICAgICBzdHJfZGV0ZWN0KHdvcmQsICJbYS16XSIpKSANCg0KdHdlZXRzX3Rva2VuaXplZA0KYGBgDQoNCiMjIHN0b3Agd29yZHMNCg0KYGBge3J9DQpoZWFkKHN0b3B3b3Jkc2xhbmdzKQ0KYGBgDQoNCg0KYGBge3J9DQp0d2VldHNfdG9rZW5pemVkICU+JSANCiAgY291bnQod29yZCwgc29ydCA9IFRSVUUsIG5hbWUgPSAiZnJlcSIpICU+JSANCiAgZmlsdGVyKCFzdHJfZGV0ZWN0KHdvcmQsICJeXFxAIikpICU+JSANCiAgYW50aV9qb2luKHN0b3B3b3Jkc2xhbmdzKSAgICAgICAgICAgIyBhbnRpX2pvaW4odGlkeXRleHQ6OmdldF9zdG9wd29yZHMoKSkNCmBgYA0KDQoNCg0KDQojIyBXb3JkIGZyZXF1ZW5jaWVzDQoNCiMjIyBDYWxjdWxhdGUgd29yZCBmcmVxdWVuY3kNCg0KYGBge3J9DQpmcmVxdWVuY3kgPC0gdHdlZXRzX3Rva2VuaXplZCAlPiUgDQogIGdyb3VwX2J5KHNjcmVlbl9uYW1lKSAlPiUgDQogIGNvdW50KHdvcmQsIHNvcnQgPSBUUlVFKSAlPiUgDQogIGxlZnRfam9pbih0d2VldHNfdG9rZW5pemVkICU+JSANCiAgICAgICAgICAgICAgZ3JvdXBfYnkoc2NyZWVuX25hbWUpICU+JSANCiAgICAgICAgICAgICAgc3VtbWFyaXNlKHRvdGFsID0gbigpKSkgJT4lDQogIG11dGF0ZShmcmVxID0gbi90b3RhbCkNCg0KZnJlcXVlbmN5DQpgYGANCg0KPiAiVGhpcyBpcyBhIG5pY2UgYW5kIHRpZHkgZGF0YSBmcmFtZSBidXQgd2Ugd291bGQgYWN0dWFsbHkgbGlrZSB0byBwbG90IHRob3NlIGZyZXF1ZW5jaWVzIG9uIHRoZSB4LSBhbmQgeS1heGVzIG9mIGEgcGxvdCwgc28gd2Ugd2lsbCBuZWVkIHRvIHVzZSBzcHJlYWQoKSBmcm9tIHRpZHlyIG1ha2UgYSBkaWZmZXJlbnRseSBzaGFwZWQgZGF0YSBmcmFtZS4gLS0gaHR0cHM6Ly93d3cudGlkeXRleHRtaW5pbmcuY29tL3R3aXR0ZXIuaHRtbA0KDQpwaXZvdF93aWRlcg0KDQpgYGB7cn0NCmZyZXF1ZW5jeSA8LSBmcmVxdWVuY3kgJT4lIA0KICBzZWxlY3Qoc2NyZWVuX25hbWUsIHdvcmQsIGZyZXEpICU+JSANCiAgcGl2b3Rfd2lkZXIobmFtZXNfZnJvbSA9IHNjcmVlbl9uYW1lLCB2YWx1ZXNfZnJvbSA9IGZyZXEpICMsIHZhbHVlc19maWxsID0gMCkNCg0KZnJlcXVlbmN5IA0KYGBgDQoNCiMjIyB2aXogaXQNCg0KYGBge3Igd29yZC1jbG91ZH0NCnR3ZWV0c190b2tlbml6ZWQgJT4lIA0KICAjIGdyb3VwX2J5KHNjcmVlbl9uYW1lKSAlPiUgDQogIGNvdW50KHdvcmQsIHNvcnQgPSBUUlVFLCBuYW1lID0gImZyZXEiKSAlPiUgDQogIGhlYWQoNTAwKSAlPiUNCiAgZmlsdGVyKCFzdHJfZGV0ZWN0KHdvcmQsICJeXFxAIikpICU+JSANCiAgYW50aV9qb2luKHN0b3B3b3Jkc2xhbmdzKSAgJT4lIA0KICB3b3JkY2xvdWQyKHNpemUgPSAyKQ0KDQpgYGANCg0KYGBge3Igd29yZC1mcmVxIGJhcnBsb3QsIGZpZy5oZWlnaHQ9N30NCnR3ZWV0c190b2tlbml6ZWQgJT4lIA0KICBjb3VudCh3b3JkLCBzb3J0ID0gVFJVRSwgbmFtZSA9ICJmcmVxIikgJT4lIA0KICBmaWx0ZXIoIXN0cl9kZXRlY3Qod29yZCwgIl5cXEAiKSkgJT4lIA0KICBzbGljZV9oZWFkKG4gPSAzMCkgJT4lIA0KICBnZ3Bsb3QoYWVzKGZyZXEsIGZjdF9yZW9yZGVyKHdvcmQsIGZyZXEpKSkgKw0KICBnZW9tX2NvbCgpDQoNCnR3ZWV0c190b2tlbml6ZWQgJT4lIA0KICBjb3VudCh3b3JkLCBzb3J0ID0gVFJVRSwgbmFtZSA9ICJmcmVxIikgJT4lIA0KICBhbnRpX2pvaW4oc3RvcHdvcmRzbGFuZ3MpICU+JSAgICAgICAgICAgICAgICAgICAgIyB0d2l0dGVyIHN0b3Agd29yZHMuDQogIGZpbHRlcighc3RyX2RldGVjdCh3b3JkLCAiXlxcQCIpKSAlPiUgDQogIHNsaWNlX2hlYWQobiA9IDMwKSAlPiUgDQogIGdncGxvdChhZXMoZnJlcSwgZmN0X3Jlb3JkZXIod29yZCwgZnJlcSkpKSArDQogIGdlb21fY29sKCkNCmBgYA0KDQoNCmBgYHtyIHdvcmRfZnJlcSBwbG90fQ0KZ2dwbG90KGZyZXF1ZW5jeSwgYWVzKFJHT05aQUxFWjE5NzgsIEJyaWNrRmFuYXRpY3MpKSArDQogIGdlb21faml0dGVyKGFscGhhID0gMC4xLCBzaXplID0gMi41LCB3aWR0aCA9IDAuMjUsIGhlaWdodCA9IDAuMjUpICsNCiAgZ2VvbV90ZXh0KGFlcyhsYWJlbCA9IHdvcmQpLCBjaGVja19vdmVybGFwID0gVFJVRSwgdmp1c3QgPSAxLjUpICsNCiAgc2NhbGVfeF9sb2cxMChsYWJlbHMgPSBzY2FsZXM6OnBlcmNlbnRfZm9ybWF0KCkpICsNCiAgc2NhbGVfeV9sb2cxMChsYWJlbHMgPSBzY2FsZXM6OnBlcmNlbnRfZm9ybWF0KCkpICsNCiAgZ2VvbV9hYmxpbmUoY29sb3IgPSAiZmlyZWJyaWNrIikNCg0KIyBmczo6ZGlyX2NyZWF0ZSgiaW1hZ2VzIikNCiMgZ2dzYXZlKCJpbWFnZXMvZHVrZWJhbGwucG5nIikNCg0KIyAiQ0JCQ2VudDEiIHwgc2NyZWVuX25hbWUgPT0gIkFkYW1fQnJhZGZvcmQxDQojIG1hcmNobWFkbmVzcyAgVGhlQW5keUthdHoNCg0KIyBSR09OWkFMRVoxOTc4DQojIDxkYmw+DQojIEJyaWNrRmFuYXRpY3MNCiMgPGRibD4NCg0KDQpnZ3Bsb3QoZnJlcXVlbmN5LCBhZXMoQnJpY2tGYW5hdGljcywgZmFudGFzeXNpdGUpKSArDQogIGdlb21faml0dGVyKGFscGhhID0gMC4xLCBzaXplID0gMi41LCB3aWR0aCA9IDAuMjUsIGhlaWdodCA9IDAuMjUpICsNCiAgZ2VvbV90ZXh0KGFlcyhsYWJlbCA9IHdvcmQpLCBjaGVja19vdmVybGFwID0gVFJVRSwgdmp1c3QgPSAxLjUpICsNCiAgc2NhbGVfeF9sb2cxMChsYWJlbHMgPSBzY2FsZXM6OnBlcmNlbnRfZm9ybWF0KCkpICsNCiAgc2NhbGVfeV9sb2cxMChsYWJlbHMgPSBzY2FsZXM6OnBlcmNlbnRfZm9ybWF0KCkpICsNCiAgZ2VvbV9hYmxpbmUoY29sb3IgPSAiZmlyZWJyaWNrIikNCmBgYA0KDQoNCiMjIFdvcmQgVXNhZ2UNCg0KDQpgYGB7cn0NCnR3ZWV0c19ieV90d2VldGVyICU+JSANCiAgc3VtbWFyaXNlKG1pbl9kYXRlID0gbWluKGNyZWF0ZWRfYXQpLCBtYXhfZGF0ZSA9IG1heChjcmVhdGVkX2F0KSkNCmBgYA0KDQoNCmBgYHtyfQ0Kd29yZF9yYXRpb3MgPC0gdHdlZXRzX3Rva2VuaXplZCAlPiUNCiAgIyBmaWx0ZXIoc2NyZWVuX25hbWUgPT0gIkNCQkNlbnQxIiB8IHNjcmVlbl9uYW1lID09ICJBZGFtX0JyYWRmb3JkMTQiKSAlPiUNCiAgZmlsdGVyKHNjcmVlbl9uYW1lID09ICJCcmlja0ZhbmF0aWNzIiB8IHNjcmVlbl9uYW1lID09ICJmYW50YXN5c2l0ZSIpICU+JSANCiAgZmlsdGVyKCFzdHJfZGV0ZWN0KHdvcmQsICJeQCIpKSAlPiUNCiAgY291bnQod29yZCwgc2NyZWVuX25hbWUpICU+JQ0KICBncm91cF9ieSh3b3JkKSAlPiUNCiAgZmlsdGVyKHN1bShuKSA+PSAyKSAlPiUNCiAgdW5ncm91cCgpICU+JQ0KICBwaXZvdF93aWRlcihuYW1lc19mcm9tID0gc2NyZWVuX25hbWUsIHZhbHVlc19mcm9tID0gbiwgdmFsdWVzX2ZpbGwgPSAwKSAlPiUNCiAgbXV0YXRlX2lmKGlzLm51bWVyaWMsIGxpc3QofiguICsgMSkgLyAoc3VtKC4pICsgMSkpKSAlPiUNCiAgbXV0YXRlKGxvZ3JhdGlvID0gbG9nKEJyaWNrRmFuYXRpY3MgLyBmYW50YXN5c2l0ZSkpICU+JQ0KICBhcnJhbmdlKGRlc2MobG9ncmF0aW8pKQ0KDQp3b3JkX3JhdGlvcw0KYGBgDQoNCiMjIyBlcXVhbCB1c2FnZQ0KDQpgYGB7cn0NCndvcmRfcmF0aW9zICU+JSANCiAgYXJyYW5nZShhYnMobG9ncmF0aW8pKQ0KYGBgDQoNCg0KYGBge3Igd29yZC11c2FnZX0NCndvcmRfcmF0aW9zICU+JQ0KICBncm91cF9ieShsb2dyYXRpbyA8IDApICU+JSAgIyA8IDApICU+JQ0KICB0b3BfbigxNSwgYWJzKGxvZ3JhdGlvKSkgJT4lDQogIHVuZ3JvdXAoKSAlPiUNCiAgbXV0YXRlKHdvcmQgPSByZW9yZGVyKHdvcmQsIGxvZ3JhdGlvKSkgJT4lDQogIGdncGxvdChhZXMod29yZCwgbG9ncmF0aW8sIGZpbGwgPSBsb2dyYXRpbyA8IDApKSArDQogIGdlb21fY29sKCkgKyAjc2hvdy5sZWdlbmQgPSBGQUxTRSkgKw0KICBjb29yZF9mbGlwKCkgKw0KICB5bGFiKCJsb2cgb2RkcyByYXRpbyAoQnJpY2tGYW5hdGljcy9mYW50YXN5c2l0ZSkiKSArDQogICMgc2NhbGVfZmlsbF9kaXNjcmV0ZShuYW1lID0gIiIsIGxhYmVscyA9IGMoIkJyaWNrRmFuYXRpY3MiLCAiZmFudGFzeXNpdGUiKSkgKw0KICBzY2FsZV9maWxsX2JyZXdlcihwYWxldHRlID0gIkRhcmsyIiwgbmFtZSA9ICIiLCBsYWJlbHMgPSBjKCJCcmlja0ZhbmF0aWNzIiwgImZhbnRhc3lzaXRlIikpDQpgYGANCg0KDQojIyBGYXZvcml0ZXMgYW5kIHJldHdlZXRzDQoNCmh0dHBzOi8vd3d3LnRpZHl0ZXh0bWluaW5nLmNvbS90d2l0dGVyLmh0bWwjZmF2b3JpdGVzLWFuZC1yZXR3ZWV0cw0KDQojIyBDaGFuZ2VzIGluIHdvcmQgdXNlDQoNCmh0dHBzOi8vd3d3LnRpZHl0ZXh0bWluaW5nLmNvbS90d2l0dGVyLmh0bWwjY2hhbmdlcy1pbi13b3JkLXVzZQ0KDQojIyBUZXJtIERvY3VtZW50IE1hdHJpeA0KDQpgYGANCntyfQ0KIyBkdG0gPC0gRG9jdW1lbnRUZXJtTWF0cml4KGRvY3MpIA0KDQpkdG0yIDwtIFRlcm1Eb2N1bWVudE1hdHJpeChjb3JwdXMpDQptIDwtIGFzLm1hdHJpeChkdG0yKQ0KdiA8LSBzb3J0KHJvd1N1bXMobSksZGVjcmVhc2luZz1UUlVFKQ0KZCA8LSBkYXRhLmZyYW1lKHdvcmQgPSBuYW1lcyh2KSxmcmVxPXYpDQoNCmQgPC0gZCAlPiUgDQogIHNsaWNlKDI6MjAwKQ0KDQpgYGANCg0KaHR0cHM6Ly93d3cudGlkeXRleHRtaW5pbmcuY29tL3RmaWRmLmh0bWwjdGhlLWJpbmRfdGZfaWRmLWZ1bmN0aW9uDQoNCmBgYHtyfQ0KdHdlZXRfd29yZHMgPC0gdHdlZXRzX2J5X3R3ZWV0ZXIgJT4lIA0KICBzZWxlY3Qoc2NyZWVuX25hbWUsIHRleHQsIHN0YXR1c19pZCwgdXNlcl9pZCkgJT4lICANCiAgdW5uZXN0X3Rva2Vucyh3b3JkLCB0ZXh0LCB0b2tlbiA9ICJ0d2VldHMiKSAlPiUgDQogIGZpbHRlcighc3RyX2RldGVjdCh3b3JkLCAiXlxcQCIpKSAlPiUNCiAgZmlsdGVyKCFzdHJfZGV0ZWN0KHdvcmQsICJeaHR0cCIpKSAlPiUNCiAgYW50aV9qb2luKHN0b3B3b3Jkc2xhbmdzKSAgJT4lIA0KICBjb3VudCh3b3JkLCB0d2VldGVyID0gc2NyZWVuX25hbWUsIHNvcnQgPSBUUlVFKQ0KDQp0d2VldF93b3Jkcw0KDQp0b3RhbF93b3JkcyA8LSB0d2VldF93b3JkcyAlPiUNCiAgZ3JvdXBfYnkodHdlZXRlcikgJT4lDQogIHN1bW1hcml6ZSh0b3RhbCA9IHN1bShuKSkgJT4lIA0KICBhcnJhbmdlKC10b3RhbCkNCg0KdG90YWxfd29yZHMNCg0KdHdlZXRfd29yZHMgPC0gbGVmdF9qb2luKHR3ZWV0X3dvcmRzLCB0b3RhbF93b3JkcykNCg0KdHdlZXRfd29yZHMNCmBgYA0KDQpgYGB7cn0NCnR3ZWV0X3dvcmRzICU+JSANCiAgYmluZF90Zl9pZGYod29yZCwgdHdlZXRlciwgbikNCmBgYA0KDQpgYGB7ciBmaWcuaGVpZ2h0PTEyfQ0KdHdlZXRfd29yZHMgJT4lIA0KICBiaW5kX3RmX2lkZih3b3JkLCB0d2VldGVyLCBuKSAlPiUgDQogIGFycmFuZ2UoZGVzYyh0Zl9pZGYpKSAlPiUgDQogIG11dGF0ZSh3b3JkID0gZmFjdG9yKHdvcmQsIGxldmVscyA9IHJldih1bmlxdWUod29yZCkpKSkgJT4lIA0KICBmaWx0ZXIobiA+IDEwKSAlPiUgDQogICMgZ3JvdXBfYnkodHdlZXRlcikgJT4lIA0KICAjIHRvcF9uKDIpICU+JSANCiAgIyB1bmdyb3VwKCkgJT4lIA0KICBnZ3Bsb3QoYWVzKHdvcmQsIHRmX2lkZikpICsNCiAgZ2VvbV9jb2woKSArDQogIGZhY2V0X3dyYXAofiB0d2VldGVyKSArDQogIGNvb3JkX2ZsaXAoKQ0KYGBgDQoNCg0KDQoNCg0KIyMgUmVzb3VyY2UgbGlzdA0KDQotIGh0dHA6Ly93d3cuc3RoZGEuY29tL2VuZ2xpc2gvd2lraS90ZXh0LW1pbmluZy1hbmQtd29yZC1jbG91ZC1mdW5kYW1lbnRhbHMtaW4tci01LXNpbXBsZS1zdGVwcy15b3Utc2hvdWxkLWtub3cNCg0KLSBodHRwOi8vYW50b25pby1mZXJyYXJvLmV1LnBuL3dvcmQtY2xvdWRzLWluLXItcGFja2FnZXMtd29yZGNsb3VkMi1hbmQtdG0vDQoNCi0gaHR0cHM6Ly9qcm5vbGQuZ2l0aHViLmlvL3Fzcy10aWR5L2Rpc2NvdmVyeS5odG1sI3RleHR1YWwtZGF0YQ0KDQotIGh0dHBzOi8vcnN0dWRpby1wdWJzLXN0YXRpYy5zMy5hbWF6b25hd3MuY29tLzMxODY3XzgyMzY5ODdjZjBhODQ0NGU5NjJjY2QyYWVjNDZkOWMzLmh0bWwNCg0KLSBvZiBsZXNzIHVzZQ0KDQogICAgLSBodHRwOi8vd3d3LmNvb2tib29rLXIuY29tL01hbmlwdWxhdGluZ19kYXRhL0NvbnZlcnRpbmdfYmV0d2Vlbl9kYXRhX2ZyYW1lc19hbmRfY29udGluZ2VuY3lfdGFibGVzLw0KICAgIC0gaHR0cHM6Ly93d3cuci1ibG9nZ2Vycy5jb20vaG93LXRvLWdldC10aGUtZnJlcXVlbmN5LXRhYmxlLW9mLWEtY2F0ZWdvcmljYWwtdmFyaWFibGUtYXMtYS1kYXRhLWZyYW1lLWluLXIvDQogICAgLSBodHRwczovL3d3dy5xdW9yYS5jb20vSG93LWRvLUktZ2V0LWEtZnJlcXVlbmN5LWNvdW50LWJhc2VkLW9uLXR3by1jb2x1bW5zLXZhcmlhYmxlcy1pbi1hbi1SLWRhdGFmcmFtZQ0KICAgIC0gaHR0cHM6Ly93d3cucXVvcmEuY29tL0hvdy1kby15b3UtY3JlYXRlLWEtY29ycHVzLWZyb20tYS1kYXRhLWZyYW1lLWluLVINCiAgICANCg0K